/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 *
 * @format
 */

"use strict";
var _extends =
  Object.assign ||
  function(target) {
    for (var i = 1; i < arguments.length; i++) {
      var source = arguments[i];
      for (var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
          target[key] = source[key];
        }
      }
    }
    return target;
  };
function _objectWithoutProperties(obj, keys) {
  var target = {};
  for (var i in obj) {
    if (keys.indexOf(i) >= 0) continue;
    if (!Object.prototype.hasOwnProperty.call(obj, i)) continue;
    target[i] = obj[i];
  }
  return target;
}
function _asyncToGenerator(fn) {
  return function() {
    var gen = fn.apply(this, arguments);
    return new Promise(function(resolve, reject) {
      function step(key, arg) {
        try {
          var info = gen[key](arg);
          var value = info.value;
        } catch (error) {
          reject(error);
          return;
        }
        if (info.done) {
          resolve(value);
        } else {
          return Promise.resolve(value).then(
            function(value) {
              step("next", value);
            },
            function(err) {
              step("throw", err);
            }
          );
        }
      }
      return step("next");
    });
  };
}

const JsFileWrapping = require("../ModuleGraph/worker/JsFileWrapping");

const assetTransformer = require("../assetTransformer");
const babylon = require("@babel/parser");
const collectDependencies = require("../ModuleGraph/worker/collectDependencies");
const constantFoldingPlugin = require("./worker/constant-folding-plugin");
const generateImportNames = require("../ModuleGraph/worker/generateImportNames");
const generate = require("@babel/generator").default;
const getKeyFromFiles = require("../lib/getKeyFromFiles");
const getMinifier = require("../lib/getMinifier");
const importExportPlugin = require("./worker/import-export-plugin");
const inlinePlugin = require("./worker/inline-plugin");
const inlineRequiresPlugin = require("babel-preset-fbjs/plugins/inline-requires");
const normalizePseudoglobals = require("./worker/normalizePseudoglobals");
var _require = require("@babel/core");
const transformFromAstSync = _require.transformFromAstSync;
var _require2 = require("metro-cache");
const stableHash = _require2.stableHash;
const types = require("@babel/types");
var _require3 = require("metro-source-map");
const fromRawMappings = _require3.fromRawMappings,
  toBabelSegments = _require3.toBabelSegments,
  toSegmentTuple = _require3.toSegmentTuple;

function getDynamicDepsBehavior(inPackages, filename) {
  switch (inPackages) {
    case "reject":
      return "reject";
    case "throwAtRuntime":
      const isPackage = /(?:^|[/\\])node_modules[/\\]/.test(filename);
      return isPackage ? inPackages : "reject";
    default:
      inPackages;
      throw new Error(
        `invalid value for dynamic deps behavior: \`${inPackages}\``
      );
  }
}

class JsTransformer {
  constructor(projectRoot, config) {
    this._projectRoot = projectRoot;
    this._config = config;
  }

  transform(filename, data, options) {
    var _this = this;
    return _asyncToGenerator(function*() {
      const sourceCode = data.toString("utf8");
      let type = "js/module";

      if (options.type === "asset") {
        type = "js/module/asset";
      }
      if (options.type === "script") {
        type = "js/script";
      }

      if (filename.endsWith(".json")) {
        let code = JsFileWrapping.wrapJson(sourceCode);
        let map = [];

        if (options.minify) {
          var _ref = yield _this._minifyCode(filename, code, sourceCode, map);
          map = _ref.map;
          code = _ref.code;
        }

        return { dependencies: [], output: [{ data: { code, map }, type }] };
      }

      // $FlowFixMe TODO t26372934 Plugin system
      const transformer = require(_this._config.babelTransformerPath);

      const transformerArgs = {
        filename,
        options: _extends({}, options, {
          enableBabelRCLookup: _this._config.enableBabelRCLookup,
          enableBabelRuntime: _this._config.enableBabelRuntime,
          // Inline requires are now performed at a secondary step. We cannot
          // unfortunately remove it from the internal transformer, since this one
          // is used by other tooling, and this would affect it.
          inlineRequires: false,
          projectRoot: _this._projectRoot
        }),

        plugins: [],
        src: sourceCode
      };

      const transformResult =
        type === "js/module/asset"
          ? yield assetTransformer.transform(
              transformerArgs,
              _this._config.assetRegistryPath,
              _this._config.assetPlugins
            )
          : yield transformer.transform(transformerArgs);

      // Transformers can ouptut null ASTs (if they ignore the file). In that case
      // we need to parse the module source code to get their AST.
      let ast =
        transformResult.ast ||
        babylon.parse(sourceCode, { sourceType: "module" });
      var _generateImportNames = generateImportNames(ast);
      const importDefault = _generateImportNames.importDefault,
        importAll = _generateImportNames.importAll;

      // Add "use strict" if the file was parsed as a module, and the directive did
      // not exist yet.
      const directives = ast.program.directives;

      if (
        ast.program.sourceType === "module" &&
        directives.findIndex(function(d) {
          return d.value.value === "use strict";
        }) === -1
      ) {
        directives.push(types.directive(types.directiveLiteral("use strict")));
      }

      // Perform the import-export transform (in case it's still needed), then
      // fold requires and perform constant folding (if in dev).
      const plugins = [];
      const opts = _extends({}, options, {
        inlineableCalls: [importDefault, importAll],
        importDefault,
        importAll
      });

      if (options.experimentalImportSupport) {
        plugins.push([importExportPlugin, opts]);
      }

      if (options.inlineRequires) {
        plugins.push([inlineRequiresPlugin, opts]);
      }

      if (!options.dev) {
        plugins.push([constantFoldingPlugin, opts]);
        plugins.push([inlinePlugin, opts]);
      }
      var _transformFromAstSync = transformFromAstSync(ast, "", {
        ast: true,
        babelrc: false,
        code: false,
        configFile: false,
        comments: false,
        compact: false,
        filename,
        plugins,
        sourceMaps: false
      });
      ast = _transformFromAstSync.ast;

      let dependencyMapName = "";
      let dependencies;
      let wrappedAst;

      // If the module to transform is a script (meaning that is not part of the
      // dependency graph and it code will just be prepended to the bundle modules),
      // we need to wrap it differently than a commonJS module (also, scripts do
      // not have dependencies).
      if (type === "js/script") {
        dependencies = [];
        wrappedAst = JsFileWrapping.wrapPolyfill(ast);
      } else {
        try {
          const opts = {
            asyncRequireModulePath: _this._config.asyncRequireModulePath,
            dynamicRequires: getDynamicDepsBehavior(
              _this._config.dynamicDepsInPackages,
              filename
            ),

            inlineableCalls: [importDefault, importAll],
            keepRequireNames: options.dev
          };
          var _collectDependencies = collectDependencies(ast, opts);
          dependencies = _collectDependencies.dependencies;
          dependencyMapName = _collectDependencies.dependencyMapName;
        } catch (error) {
          if (error instanceof collectDependencies.InvalidRequireCallError) {
            throw new InvalidRequireCallError(error, filename);
          }
          throw error;
        }
        var _JsFileWrapping$wrapM = JsFileWrapping.wrapModule(
          ast,
          importDefault,
          importAll,
          dependencyMapName
        );
        wrappedAst = _JsFileWrapping$wrapM.ast;
      }

      const reserved =
        options.minify && data.length <= _this._config.optimizationSizeLimit
          ? normalizePseudoglobals(wrappedAst)
          : [];

      const result = generate(
        wrappedAst,
        {
          comments: false,
          compact: false,
          filename,
          retainLines: false,
          sourceFileName: filename,
          sourceMaps: true
        },

        sourceCode
      );

      let map = result.rawMappings
        ? result.rawMappings.map(toSegmentTuple)
        : [];
      let code = result.code;

      if (options.minify) {
        var _ref2 = yield _this._minifyCode(
          filename,
          result.code,
          sourceCode,
          map,
          reserved
        );
        map = _ref2.map;
        code = _ref2.code;
      }

      return { dependencies, output: [{ data: { code, map }, type }] };
    })();
  }

  _minifyCode(filename, code, source, map) {
    var _this2 = this;
    let reserved =
      arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : [];
    return _asyncToGenerator(function*() {
      const sourceMap = fromRawMappings([
        { code, source, map, path: filename }
      ]).toMap(undefined, {});

      const minify = getMinifier(_this2._config.minifierPath);

      try {
        const minified = minify({
          code,
          map: sourceMap,
          filename,
          reserved,
          config: _this2._config.minifierConfig
        });

        return {
          code: minified.code,
          map: minified.map
            ? toBabelSegments(minified.map).map(toSegmentTuple)
            : []
        };
      } catch (error) {
        if (error.constructor.name === "JS_Parse_Error") {
          throw new Error(
            `${error.message} in file ${filename} at ${error.line}:${error.col}`
          );
        }

        throw error;
      }
    })();
  }

  getCacheKey() {
    var _config = this._config;
    const babelTransformerPath = _config.babelTransformerPath,
      minifierPath = _config.minifierPath,
      config = _objectWithoutProperties(_config, [
        "babelTransformerPath",
        "minifierPath"
      ]);

    const filesKey = getKeyFromFiles([
      require.resolve(babelTransformerPath),
      require.resolve(minifierPath),
      require.resolve("../ModuleGraph/worker/JsFileWrapping"),
      require.resolve("../assetTransformer"),
      require.resolve("../ModuleGraph/worker/collectDependencies"),
      require.resolve("./worker/constant-folding-plugin"),
      require.resolve("../lib/getMinifier"),
      require.resolve("./worker/inline-plugin"),
      require.resolve("./worker/import-export-plugin"),
      require.resolve("./worker/normalizePseudoglobals"),
      require.resolve("../ModuleGraph/worker/optimizeDependencies"),
      require.resolve("../ModuleGraph/worker/generateImportNames")
    ]);

    const babelTransformer = require(babelTransformerPath);
    const babelTransformerKey = babelTransformer.getCacheKey
      ? babelTransformer.getCacheKey()
      : "";

    return [
      filesKey,
      stableHash(config).toString("hex"),
      babelTransformerKey
    ].join("$");
  }
}

class InvalidRequireCallError extends Error {
  constructor(innerError, filename) {
    super(`${filename}:${innerError.message}`);
    this.innerError = innerError;
    this.filename = filename;
  }
}

module.exports = JsTransformer;
